package ika.mapanalyst;

import ika.geo.osm.Projector;
import ika.proj.Ellipsoid;
import ika.proj.Projection;
import ika.proj.ProjectionFactory;
import ika.proj.ProjectionsManager;
import ika.transformation.Transformation;
import ika.utils.TextWindow;
import java.text.DecimalFormat;
import java.util.Vector;

/**
 *
 * @author jenny
 */
public class ProjectionEvaluator {

    private Projection p;
    private Transformation transformation;
    private double[][] geoNewPointsRad;
    private double[][] oldPoints;
    private double deltaLon0Deg = 0.25;
    private String lastFitReport = "";
    private static final DecimalFormat formatter = new DecimalFormat("#,###.0");
    private static final String nl = System.getProperty("line.separator");

    public ProjectionEvaluator() {
    }

    public Projection getBestFit(double[][] osmPoints, double[][] oldPoints,
            Transformation transformation) {

        assert (osmPoints.length == oldPoints.length);

        this.oldPoints = oldPoints;
        this.transformation = transformation;

        // convert new points to geographic coordinates
        geoNewPointsRad = new double[osmPoints.length][2];
        Projector.OSM2Geo(osmPoints, geoNewPointsRad);

        StringBuilder report = new StringBuilder();
        appendHeaderInfo(report);

        Vector<String> projections = ProjectionsManager.getProjectionNames();
        double smallestDist = Double.MAX_VALUE;
        Projection bestFitProjection = null;
        
        double[] longitudeRange = longitudeRangeDegrees(geoNewPointsRad);
        for (String projName : projections) {
            p = ProjectionFactory.getNamedProjection(projName);
            double meanDist = fitLongitude(longitudeRange);
            if (meanDist < smallestDist) {
                smallestDist = meanDist;
                bestFitProjection = p;
            }
            appendProjectionAndTransformationInfo(report, meanDist);
        }

        lastFitReport = report.toString();
        return bestFitProjection;
    }

    private void appendHeaderInfo(StringBuilder sb) {
        sb.append("Projection Comparison");
        sb.append(nl);
        sb.append("---------------------");
        sb.append(nl);
        sb.append(nl);
        sb.append("Mean distance for all control points");
        sb.append(nl);
        sb.append("------------------------------------");
        sb.append(nl);
        sb.append(nl);
    }

    private void appendProjectionAndTransformationInfo(StringBuilder sb, double meanDist) {
        sb.append(p.toString());
        sb.append(": \t");
        sb.append(formatter.format(meanDist * 1000));
        sb.append(" mm");
        sb.append(nl);
        sb.append("Central Longitude: \t");
        sb.append(formatter.format(p.getProjectionLongitudeDegrees()));
        sb.append("\u00B0");
        sb.append(nl);
        sb.append("with ");
        sb.append(transformation.getName());
        sb.append(" transformation");
        sb.append(nl);
        sb.append(transformation.getShortReport(true));
        sb.append(nl);
    }

    public double evalProjection(Projection p,
            double[][] osmPoints, double[][] oldPoints,
            Transformation transformation) {

        this.p = p;
        this.oldPoints = oldPoints;
        this.transformation = transformation;

        // convert new points to geographic coordinates
        geoNewPointsRad = new double[osmPoints.length][2];
        Projector.OSM2Geo(osmPoints, geoNewPointsRad);

        return evalProjection();
    }
    
    private double evalProjection() {

        final int nPoints = oldPoints.length;
        double[][] projectedSourcePoints = new double[nPoints][2];
        Projector projector = new Projector();
        projector.setInitializedProjection(p);
        projector.geo2Intermediate(geoNewPointsRad, projectedSourcePoints);

        transformation.init(oldPoints, projectedSourcePoints);
        double[][] v = transformation.getResiduals();
        double dSum = 0;
        for (int i = 0; i < v.length; i++) {
            dSum += Math.hypot(v[i][0], v[i][1]);
        }
        System.out.println(dSum / nPoints);
        return dSum / nPoints;
    }

    public static void initProjection(Projection p, double lon0Deg) {
        p.setProjectionLongitudeDegrees(lon0Deg);
        p.setEllipsoid(Ellipsoid.SPHERE);
        //p.setTrueScaleLatitude(Math.toRadians(lon0Deg));
        p.initialize();
    }

    private double fitLongitude(double[] lonRangeDeg) {

        System.out.println(p.toString());
        double smallestDist = Double.MAX_VALUE;
        double bestFitLon0 = lonRangeDeg[0];
        for (double lon0 = lonRangeDeg[0]; lon0 <= lonRangeDeg[1]; lon0 += deltaLon0Deg) {
            initProjection(p, lon0);
            double d = evalProjection();
            if (d < smallestDist) {
                smallestDist = d;
                bestFitLon0 = lon0;
            }
        }

        initProjection(p, bestFitLon0);
        
        return smallestDist;
    }

    private static double[] longitudeRangeDegrees(double[][] ptsRad) {

        if (ptsRad == null || ptsRad.length == 0) {
            return new double[]{0, 0};
        }

        double lon0Min = Double.MAX_VALUE;
        double lon0Max = -Double.MAX_VALUE;
        for (int i = 0; i < ptsRad.length; i++) {
            lon0Max = Math.max(ptsRad[i][0], lon0Max);
            lon0Min = Math.min(ptsRad[i][0], lon0Min);
        }
        return new double[]{Math.toDegrees(lon0Min), Math.toDegrees(lon0Max)};

    }

    /**
     * @return the lastFitReport
     */
    public String getLastFittingReport() {
        return lastFitReport;
    }
}
